Skip to content

feat: granular text selection + workflow on-response example - #55

Merged
yogthos merged 1 commit into
mainfrom
feat/granular-selection
May 20, 2026
Merged

feat: granular text selection + workflow on-response example#55
yogthos merged 1 commit into
mainfrom
feat/granular-selection

Conversation

@yogthos

@yogthos yogthos commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Switches chat selection from line-granular to character-granular: anchors are now (line_idx, char_col) pairs, mouse coords map through new Renderer::buffer_pos_at (clamps at EOL), and viewport paint splits each row into up-to-three runs so highlighting is per-character. Plus a new plugins/response_inspector.janet example demonstrating on-response notifications + return-string steering. 612 tests pass.

Two items from the deferred audit list.

## Granular text selection

Selection model was line-only: clicking a chat line selected the
whole line, dragging extended row-by-row. Users couldn't select a
substring within a single line (a paragraph fragment, half a
filename, the right half of a diff hunk).

Selection anchors are now `(buffer_line_index, char_offset_in_line)`
pairs instead of `Option<usize>`. New `Renderer::buffer_pos_at(row,
col) -> Option<(usize, usize)>` maps mouse coordinates to a buffer
position, accounting for `content_indent()` and clamping at
end-of-line so dragging past the right edge anchors there.

The viewport paint splits each row into up to three painted runs
(pre-selection, selected with `Attribute::Reverse`, post-selection)
so highlighting happens per-character within a line. Bold-glow
for bright colors is preserved across all three runs.

`selected_text` extracts:
- single-row: char slice [start_col, end_col)
- multi-row: tail of start row + full middle rows + head of end row

Reverse drag (end before start in row-major order) is normalized
so the extracted text is always the visible selection.

Mouse handlers (`MouseDown`, `MouseDrag`, `MouseUp`) now pass
`col` through to `buffer_pos_at` instead of dropping it.

UTF-8 safe: char counts use `chars().count()` everywhere, so
`é` and `🦀` each count as 1 char, not their byte widths.

## workflow.janet on-response example

New `plugins/response_inspector.janet` demonstrates the
`on-response` hook's two distinct capabilities:

1. **Pattern detection + notifications** — calls `harness/notify`
   when the agent's reply contains a code block.
2. **Steering string return value** — returns a system-prompt
   suffix to be applied on the next turn, asking the agent to
   add inline comments when its previous code block lacked them.

Documents the difference between `on-response` (assistant text)
and `on-tool-end` (tool output, where `harness/replace-result`
belongs).

## Test plan

- [x] 6 new tests in `ui::renderer::tests`:
  - single-row substring selection
  - reverse-drag normalization
  - multi-row spans (tail + middle + head)
  - empty selection returns None
  - UTF-8 char-index correctness (`café 🦀`)
  - `buffer_pos_at` clamps past-EOL
- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
- [ ] Eyeball: drag in chat to select a substring; verify
      reverse-video highlight only covers the selected chars,
      clipboard receives the right substring.
@yogthos
yogthos merged commit 7d4c6ce into main May 20, 2026
@yogthos
yogthos deleted the feat/granular-selection branch May 20, 2026 23:20
allen-munsch pushed a commit to allen-munsch/dirge that referenced this pull request Jun 3, 2026
…ode#55)

Two items from the deferred audit list.

## Granular text selection

Selection model was line-only: clicking a chat line selected the
whole line, dragging extended row-by-row. Users couldn't select a
substring within a single line (a paragraph fragment, half a
filename, the right half of a diff hunk).

Selection anchors are now `(buffer_line_index, char_offset_in_line)`
pairs instead of `Option<usize>`. New `Renderer::buffer_pos_at(row,
col) -> Option<(usize, usize)>` maps mouse coordinates to a buffer
position, accounting for `content_indent()` and clamping at
end-of-line so dragging past the right edge anchors there.

The viewport paint splits each row into up to three painted runs
(pre-selection, selected with `Attribute::Reverse`, post-selection)
so highlighting happens per-character within a line. Bold-glow
for bright colors is preserved across all three runs.

`selected_text` extracts:
- single-row: char slice [start_col, end_col)
- multi-row: tail of start row + full middle rows + head of end row

Reverse drag (end before start in row-major order) is normalized
so the extracted text is always the visible selection.

Mouse handlers (`MouseDown`, `MouseDrag`, `MouseUp`) now pass
`col` through to `buffer_pos_at` instead of dropping it.

UTF-8 safe: char counts use `chars().count()` everywhere, so
`é` and `🦀` each count as 1 char, not their byte widths.

## workflow.janet on-response example

New `plugins/response_inspector.janet` demonstrates the
`on-response` hook's two distinct capabilities:

1. **Pattern detection + notifications** — calls `harness/notify`
   when the agent's reply contains a code block.
2. **Steering string return value** — returns a system-prompt
   suffix to be applied on the next turn, asking the agent to
   add inline comments when its previous code block lacked them.

Documents the difference between `on-response` (assistant text)
and `on-tool-end` (tool output, where `harness/replace-result`
belongs).

## Test plan

- [x] 6 new tests in `ui::renderer::tests`:
  - single-row substring selection
  - reverse-drag normalization
  - multi-row spans (tail + middle + head)
  - empty selection returns None
  - UTF-8 char-index correctness (`café 🦀`)
  - `buffer_pos_at` clamps past-EOL
- [x] `cargo test --features plugin` -> 612 pass, 0 fail.
- [x] Both build profiles -> 0 warnings.
- [ ] Eyeball: drag in chat to select a substring; verify
      reverse-video highlight only covers the selected chars,
      clipboard receives the right substring.

Co-authored-by: Yogthos <yogthos@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant